/*
 * Copyright (c) 2010-2011 Josef Hardi <josef.hardi@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package id.duapuluhdua.dakidaki.layers;

import java.util.Observable;
import java.util.Observer;

import android.app.Service;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Vibrator;
import android.util.DisplayMetrics;
import android.util.FloatMath;
import android.view.ViewConfiguration;

import id.duapuluhdua.dakidaki.MainView;
import id.duapuluhdua.dakidaki.TileMapManager;
import id.duapuluhdua.dakidaki.activities.ActivityMain;
import id.duapuluhdua.dakidaki.common.CommonActivity;
import id.duapuluhdua.dakidaki.map.Screen;
import id.duapuluhdua.dakidaki.map.TileMap;
import id.duapuluhdua.dakidaki.map.TileUtils;

/**
 *
 */
public class LayerMap extends Layer implements Observer
{
  /** Duration in ms for the phone vibration. */
  private final static int VIBRATE_TIME = 50;

  /** Keys for map controls. */
  private enum Mode {
    UNDEFINED, PAN, ZOOM
  }

  /** Fields */
  private MainView mView;

  private Paint mTile;
  private Paint mGrayFill;
  private Paint mWhiteFill;

  private Rect mRtTile;
  private RectF mRtTileDraw;

  private TileMapManager mTileManager;
  private Vibrator mVibrator;

  private double mInitialLongitude;
  private double mInitialLatitude;

  private double mLongitude;
  private double mLatitude;

  private float mTouchX;
  private float mTouchY;

  private int mScrollDistance;
  private int mZoom = TileMap.MIN_ZOOM;

  private Mode mMode = Mode.UNDEFINED;

  /** The density of the device */
  private final float mDisplayDensity;

  /** Duration in ms before a press turns into a long press */
  private final int mLongPressTimeout;

  /**
   * Construct the layer map.
   *
   * @param parent
   *            The parent activity.
   * @param screen
   *            The screen object.
   */
  public LayerMap(CommonActivity parent, Screen screen)
  {
    super(parent, screen);

    mView = ((ActivityMain)parent).getView();

    mRtTile = new Rect();
    mRtTileDraw = new RectF();

    // Paint properties for drawing tiles.
    mTile = new Paint();
    mTile.setFilterBitmap(true);

    // Paint properties for drawing empty tiles.
    mGrayFill = new Paint();
    mGrayFill.setColor(Color.GRAY);
    mGrayFill.setStyle(Style.FILL);
    mGrayFill.setAntiAlias(true);

    mWhiteFill = new Paint();
    mWhiteFill.setColor(Color.WHITE);
    mWhiteFill.setStyle(Style.FILL);
    mWhiteFill.setAntiAlias(true);

    DisplayMetrics mtDisplay = new DisplayMetrics();
    parent.getWindowManager().getDefaultDisplay().getMetrics(mtDisplay);
    mDisplayDensity = mtDisplay.density;

    mTileManager = parent.getTileManager();

    mLongPressTimeout = ViewConfiguration.getLongPressTimeout();
    mVibrator = (Vibrator)mContext.getSystemService(Service.VIBRATOR_SERVICE);

    // Observe location status
    super.startObserveLocation(this);

    initData();
  }

  @Override
  public void initData()
  {
    // Default location: 39100 Bozen-Bolzano
    final double longitude = 11.35934;
    final double latitude = 46.501819;

    setAnchorPoint(latitude, longitude);
  }

  /**
   * Set the touch mode.
   *
   * @param mode
   *            There are three options (Mode.UNDEFINED, Mode.PAN and Mode.ZOOM).
   */
  public void setTouchMapMode(Mode mode)
  {
    mMode = mode;
  }

  /**
   * The x-coordinate of the center of the screen is calculated by taking the
   * half of the screen width.
   *
   * @return Returns the x-coordinate.
   */
  public int getCenterScreenX()
  {
    return mScreen.getWidth() / 2;
  }

  /**
   * The y-coordinate of the center of the screen is calculated by taking the
   * half of the screen height.
   *
   * @return Returns the y-coordinate.
   */
  public int getCenterScreenY()
  {
    return mScreen.getHeight() / 2;
  }

  /**
   * Set the anchor point for the map. Following the drawing strategy, the
   * anchor point is placed at the center of the screen.
   *
   * @param latitude
   *            The latitude coordinate.
   * @param longitude
   *            The longitude coordinate.
   */
  public void setAnchorPoint(double latitude, double longitude)
  {
    mLatitude = latitude;
    mLongitude = longitude;
  }

  /**
   * Set the zoom level (min: 10 and max: 14).
   *
   * Every time the zoom level is set, the anchor point is re-calculated
   * to ensure the zoom anchor doesn't change. A "zoom anchor" is the
   * coordinate spot on the map produced by the user's finger tap when he
   * touches the screen to perform the zooming gesture.
   *
   * @param zoom
   *            The zoom level.
   */
  public void setZoom(int zoom)
  {
    if (zoom > TileMap.MAX_ZOOM) {
      mZoom = TileMap.MAX_ZOOM;
    }
    else if (zoom < TileMap.MIN_ZOOM) {
      mZoom = TileMap.MIN_ZOOM;
    }
    else {
      mZoom = zoom;
    }
    anchorPointChanged();
  }

  /**
   * To have the "zoom anchor" fixed in the map while zooming, we have to
   * update the screen anchor point dynamically such that the distance 
   * between the screen anchor and the zoom anchor remains the same.
   *
   * To do this, first we measure the distance between the screen anchor and
   * the zoom anchor. (Note: the screen anchor is at the center of the screen).
   *
   * Second, we calculate this distance with respect to the tile size,
   * that is, we change the unit measurement from pixel to tile units.
   *
   * Third, we compute the tile coordinate (Tx, Ty) for the zoom anchor,
   * taking into account the (lat, lon) coordinate and the zoom level.
   *
   * And finally, we re-calculate the new screen anchor such that the new
   * position doesn't modify the zoom anchor during the redraw.
   */
  private void anchorPointChanged()
  {
    // First step
    final float dx = mTouchX - getCenterScreenX(); // in px
    final float dy = mTouchY - getCenterScreenY();

    // Second step
    final float dTx = getNTileX(dx);
    final float dTy = getNTileY(dy);

    // Third step
    final float Tx = (float)TileUtils.getTileX(mZoom, mInitialLongitude);
    final float Ty = (float)TileUtils.getTileY(mZoom, mInitialLatitude);

    // Final step
    double latitude = TileUtils.getLatitude(mZoom, Ty - dTy);
    double longitude = TileUtils.getLongitude(mZoom, Tx - dTx);

    setAnchorPoint(latitude, longitude);
  }

  /**
   * Calculate the tile size considering the screen density.
   *
   * @return The size of each tile image (in px).
   */
  private float getTileDrawingSize()
  {
    float size = TileMap.TILE_SIZE;

    if (mDisplayDensity > 1f) {
      size = size * mDisplayDensity;
    }
    return size;
  }

  /**
   * Counts the number of tiles that fits to the length of dx.
   *
   * @param dx
   *            The length measure (in px).
   * @return The number of tiles (x-index)
   */
  private float getNTileX(float dx)
  {
    return dx / getTileDrawingSize();
  }

  /**
   * Counts the number of tiles that fits to the length of dy.
   *
   * @param dy
   *            The length measure (in px).
   * @return The number of tiles (y-index).
   */
  private float getNTileY(float dy)
  {
    return dy / getTileDrawingSize();
  }

  /**
   * Calculate the longitude given the screen point x (in px). The calculation
   * uses the anchor point as the base longitude. And by knowing the distance
   * between the anchor point and the point in question, we can estimate
   * its longitude measure.
   *
   * @param x
   *            The point in question in x-axis.
   * @return The longitude measure.
   */
  private double getLongitudeFromPoint(float x)
  {
    final float dx = x - getCenterScreenX();
    final float Tx = (float)TileUtils.getTileX(mZoom, mLongitude);
    final float dTx = getNTileX(dx);

    return TileUtils.getLongitude(mZoom, Tx + dTx);
  }

  /**
   * Calculate the latitude given the screen point y (in px). The calculation
   * uses the anchor point as the base latitude. And by knowing the distance
   * between the anchor point and the point in question, we can estimate
   * its latitude measure.
   *
   * @param y
   *            The point in question in y-axis.
   * @return The latitude measure.
   */
  private double getLatitudeFromPoint(float y)
  {
    final float dy = y - getCenterScreenY();
    final float Ty = (float)TileUtils.getTileY(mZoom, mLatitude);
    final float dTy = getNTileY(dy);

    return TileUtils.getLatitude(mZoom, Ty + dTy);
  }

  @Override
  public void doDraw(Canvas canvas, Rect area)
  {
    final float fTSize = getTileDrawingSize();

    final float mX = getCenterScreenX();  // middle X
    final float mY = getCenterScreenY();  // middle Y

    final float dTx = getNTileX(mX);  // delta tile X
    final float dTy = getNTileY(mY);  // delta tile Y

    final float Tx = (float) TileUtils.getTileX(mZoom, mLongitude); // tile X
    final float Ty = (float) TileUtils.getTileY(mZoom, mLatitude);  // tile Y

    int Tx0 = (int) FloatMath.floor(Tx - dTx);  // start tile X
    int Ty0 = (int) FloatMath.floor(Ty - dTy);  // start tile Y
    int nTx = (int) FloatMath.ceil((Tx + dTx) - Tx0);  // number of tile X
    int nTy = (int) FloatMath.ceil((Ty + dTy) - Ty0);  // number of tile Y

    for (int i = 0; i < nTx; i++) {
      for (int j = 0; j < nTy; j++) {
        int iTx = Tx0 + i;  // tile X in succession
        int iTy = Ty0 + j;  // tile Y in succession
        float pX = ((iTx - Tx) * fTSize) + mX;  // point X
        float pY = ((iTy - Ty) * fTSize) + mY;  // point Y

        Bitmap tile = mTileManager.getTileImage(iTx, iTy, mZoom, false);

        if (tile == null) {
          drawEmptyTile(canvas, pX, pY, fTSize);
        }
        else {
          mRtTile.set(0, 0, TileMap.TILE_SIZE, TileMap.TILE_SIZE);
          mRtTileDraw.set(pX, pY, pX + fTSize, pY + fTSize);
          canvas.drawBitmap(tile, mRtTile, mRtTileDraw, mTile);
        }
      }
    }
  }

  /**
   * Draw a gray and white tile to indicate an empty tile.
   */
  private void drawEmptyTile(Canvas canvas, float x, float y, float tileSize)
  {
    final int numberEmptyTiles = 16;

    float tileDiv = (tileSize / numberEmptyTiles);
    for (int k1 = 0; k1 < numberEmptyTiles; k1++) {
      for (int k2 = 0; k2 < numberEmptyTiles; k2++) {
        float xk = x + tileDiv * k1;
        float yk = y + tileDiv * k2;
        if ((k1 + k2) % 2 == 0) {
          canvas.drawRect(xk, yk, xk + tileDiv, yk + tileDiv, mGrayFill);
        }
        else {
          canvas.drawRect(xk, yk, xk + tileDiv, yk + tileDiv, mWhiteFill);
        }
      }
    }
  }

  /**
   * Save the coordinate when user initially touches the screen. In addition,
   * the (lat, lon) coordinate relative to the anchor point is estimated.
   *
   * @param x
   *            The screen x coordinate.
   * @param y
   *            The screen y coordinate.
   */
  public void registerTouchPoint(float x, float y)
  {
    mTouchX = x;
    mTouchY = y;

    mInitialLongitude = getLongitudeFromPoint(x);
    mInitialLatitude = getLatitudeFromPoint(y);
  }

  /**
   * Calculate the new anchor point if user pans the map or set the new zoom
   * level if user performs zooming.
   *
   * @param dx
   *            The finger movement in x-axis.
   * @param dy
   *            The finger movement in y-axis.
   */
  public void onScroll(float dx, float dy)
  {
    if (mMode == Mode.PAN) {
      final float Tx = (float) TileUtils.getTileX(mZoom, mLongitude);
      final float Ty = (float) TileUtils.getTileY(mZoom, mLatitude);

      final float dTx = getNTileX(dx);
      final float dTy = getNTileY(dy);

      final double longitude = TileUtils.getLongitude(mZoom, Tx + dTx);
      final double latitude = TileUtils.getLatitude(mZoom, Ty + dTy);

      setAnchorPoint(latitude, longitude);

      // Update the viewport so that the points in the screen got adjusted.
      ActivityMain.loader.shiftViewport(dx, -dy);
    }
    else if (mMode == Mode.ZOOM) {
      mScrollDistance += dy;
      if (mScrollDistance % 10 == 0) {
        int gain = (dy >= 0) ? 1 : -1;
        setZoom(mZoom + gain);
      }
    }
  }

  /**
   * The actions if a user does a long press gesture.
   */
  private final Runnable mLongPressed = new Runnable() {
    @Override
    public void run()
    {
      mScrollDistance = 0; // reset the scroll distance for zooming.
      setTouchMapMode(Mode.ZOOM);
      mVibrator.vibrate(VIBRATE_TIME);
    }
  };

  /**
   * Register the long press call back.
   */
  public void postLongPressCallback()
  {
    setTouchMapMode(Mode.PAN);  // initially it's on PAN mode.
    mView.postDelayed(mLongPressed, mLongPressTimeout);
  }

  /**
   * Remove the long press call back.
   */
  public void removeLongPressCallback()
  {
    mView.removeCallbacks(mLongPressed);
  }

  @Override
  public void update(Observable observable, Object data)
  {
    super.updateLocation(observable, data);
  }

  @Override
  public void updateLocation()
  {
    setAnchorPoint(mLocationGeoPoint.wgsLat, mLocationGeoPoint.wgsLon);
  }

  @Override
  public void surfaceSizeChanged(int width, int height, int screenOrientation)
  {
    // Do nothing.
  }

  @Override
  public void updateObjectsState()
  {
    // Do nothing.
  }
}
